Skip to content

[OMNIML-6063] Decompress compressed-tensors MoE experts on the fly as they load - #2747

Open
shengliangxu wants to merge 16 commits into
shengliangx/hfep-9-ep-model-specsfrom
shengliangx/hfep-11a-onfly-decompress
Open

shengliangxu wants to merge 16 commits into
shengliangx/hfep-9-ep-model-specsfrom
shengliangx/hfep-11a-onfly-decompress

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Note

The expert-parallel PTQ series is now a tree instead of one stack (re-cut 2026-10-27, from main 1521053), so PRs that do not depend on each other can be reviewed and merged in parallel. This PR was [18/21].

Base: #2746. The diff below is this PR's alone, and it merges after #2746.

Re-cut: this PR's diff is unchanged; only its base moved. Review fixes since are listed at the end of Testing.

Series layout

What does this PR do?

Type of change: New feature

Expert-parallel PTQ loads a checkpoint with from_pretrained over the expert-parallel mesh, so each rank builds only its own experts. A checkpoint whose experts are compressed-tensors quantized (Kimi-K2.5's INT4, Kimi-K3's MXFP4) cannot take that load as it is: transformers' fused-experts conversion expects each expert's plain .weight.

  • _onfly_decompress rewrites those per-expert converters to read the checkpoint's stored keys (weight_packed, weight_scale, ...). It prepends a step that dequantizes each owned expert to bf16 before the merge, so PTQ runs on the original compressed checkpoint with no decompressed copy on disk.
    • It is format-agnostic: the compressed_tensors library does the math, driven by the checkpoint's own quantization_config, for any format whose stored floats bf16 holds exactly; a checkpoint whose compressed experts store fp16, fp32 or fp64 (such as NVFP4's fp32 global scale) is refused, since the load may cast it to bf16 before decompressing.
    • It handles a checkpoint whose experts are its only compressed weights, in one compressed-tensors config group that targets the experts (a class target such as Linear, or a regex naming the experts), and refuses one with several; a quantization_config with no groups is refused as nothing to decompress. Kimi-K2.5, K2.6 and K2.7-Code (INT4) and Kimi-K3 (MXFP4) each carry one group, under text_config, with an ignore list keeping the rest bf16.
    • The expert-parallel load checks the checkpoint's stored tensor names before it loads (check_compressed_keys): every weight stored compressed must be an expert's, and every expert's weight stored compressed, since a class target such as Linear does not say what was compressed. The Kimi-K2.5, K2.6, K2.7-Code and K3 checkpoints pass it. It also reads, from the shard headers, the dtype of every tensor the compressed experts store, and refuses a float bf16 cannot hold exactly. Kimi-K2.5, K2.6 and K2.7-Code store int32 packed weights and shapes with bf16 scales, and Kimi-K3 uint8 packed weights and scales (one shard header each), so all four pass that too.
    • A checkpoint key the decompression does not consume fails the load, as transformers' conversion error, instead of reaching the merge.
    • Decompression pairs each expert's stored tensors by position, and refuses stored params that disagree on how many experts they hold.
    • A config with nothing to decompress raises ValueError before anything is patched, and without needing compressed_tensors installed; a raw dict config raises TypeError there too, since the load finds the hooks by a mark only a config object carries. A quantization_config that fails to parse raises its own error, on a second install too, rather than pass for no config: the experts would otherwise load newly initialized.
    • It needs transformers 5.8: earlier releases' Concatenate looks up the sibling keys the decompression has already consumed.
    • It also holds the helper that rebuilds a VLM's bundled text config as a native transformers backbone config.
    • Its mark holds the names a compressed weight is stored under, which the load's check reads. The mark and the compressed-tensors config are read on the top level, text_config and llm_config, the levels the rest of the expert-parallel load reads; the hook refuses a marked model whose conversion mapping has no per-expert converter to rewrite (its experts would load newly initialized), and clears the mark at every level only once it has rewritten them. to_native_backbone_config carries the mark to the backbone config it derives.
  • A decompressed load no longer hands from_pretrained the checkpoint's compressed-tensors quantization_config: on the marked path the expert-parallel load deletes it from its copy of the config, at the top level, on text_config and on llm_config, every level _ckpt_quant_method reads, so transformers builds no compressed-tensors quantizer to compete with the decompressor. Kimi-K2.5's config class copies its text config's quantization_config to the top level, and from transformers 5.13 that quantizer took over the expert converters itself.
  • _remote_code.stage_full_remote_closure makes transformers stage every module of a local checkpoint's remote code, not only those one import deep, which loading the Kimi checkpoints' own modeling needs. It stages each module through a temp file renamed into place, so ranks staging the same checkpoint at once never import a half-written module.
  • CI's unit nox sessions install compressed-tensors on torch 2.10 and later, the releases it supports, so the tests against the real library run there. It goes in no extra: it pulls in transformers, which CI's transformers-free lane must not get.

Experts arrive in numeric order without help: every transformers from 5.5 sorts the state dict numerically (dot_natural_key) before it collects tensors. The rewritten converter keeps the scope transformers sets on it (scope_prefix, base_model_prefix, force_cpu).

Usage

Internal to the Kimi adapters (#2748, #2749), called from their prepare hooks.

Testing

  • New tests/unit/torch/models/test_onfly_decompress.py (39 cases). It covers:

    • the rewritten converters (anchored stored keys, gate before up), and a source the decompression does not consume failing the load;
    • a marked model with no per-expert converter to rewrite, refused and still marked;
    • packed experts decompressing and fusing like an uncompressed checkpoint, through the loader's own steps;
    • only owned experts reaching the compressor, and stored params disagreeing on the expert count refused;
    • integer tensors keeping their dtype, through transformers' own copy;
    • installing once, and a config with nothing to decompress, or a raw dict, refused with the loader left alone and compressed_tensors blocked;
    • the stored-tensor check over seven layouts: the experts alone compressed pass, in a packed and in a float format; every Linear compressed, all but the experts, an expert left uncompressed, nothing compressed, or an fp32 scale refused;
    • a VLM marked at either level, loaded whole or as its backbone, through the rewritten converters, neither level keeping the mark;
    • a compressed-tensors config and a mark on llm_config, read as on text_config;
    • with compressed_tensors installed, a checkpoint in several config groups, in one group that is not the experts, or in none refused, and an INT4 pack-quantized round trip. These 5 cases run in CI's unit sessions on torch 2.10 and later; elsewhere they skip, saying what they need.
  • Mutation-checked: each test fails on at least one deliberate break of the code, and each of these fails its test: the stored-name check not called in the load, llm_config not cleared, each of the check's three refusal clauses removed, experts matched by substring (shared_experts then counts), weight counted as compressed, no raw-dict refusal, no refusal of a mapping with nothing to rewrite, the mark cleared before the rewrite, no expert-count check, text_config as the only nested level, and no dtype check.

  • Skips below transformers 5.8, the release the decompressor requires, so CI's 5.5 job skips it and its 5.18 job runs it. Run locally on 5.12.

  • A second, differently compressed checkpoint in the same process raises rather than reusing the first one's decompression; the same checkpoint again is a no-op, and one whose quantization_config fails to parse raises. Any other model loaded in the process keeps its stock converters: only the marked checkpoint's are rewritten.

  • The source-pattern checks ride the packed-load test, the other-model and copy-load tests are one, and the helper-level ValueError test is dropped. Each drop is checked by mutation.

  • New tests/unit/torch/models/test_remote_code.py: a three-module import chain staged in full, and installed once; a copy cut short leaves no module behind, and the next load stages it whole.

  • tests/unit/torch/utils/test_ep_loader.py: the compressed-checkpoint case loads from a small safetensors checkpoint, which stores int32 packed weights and bf16 scales as the real ones do, and runs the load to from_pretrained, whose config has no quantization_config at any level, llm_config's included, while the caller's config keeps its own; one that also compresses an attention projection is refused before from_pretrained.

  • The stored-name check, run read-only on the tensor names of the real Kimi-K2.5, K2.6, K2.7-Code and K3 checkpoints (their index and shard headers): all four pass. Reading the stored names, and with them the compressed experts' dtypes, takes, per rank:

    Checkpoints Names Names and dtypes
    Kimi-K2.5, K2.6, K2.7-Code, K3 0.5 s to 2.2 s (the most: K3's 497,220 names, read cold) not timed
    Synthetic, K3's 247,500 compressed tensors in 10 shards (CPU) 0.5 s 1.0 s
  • Locally on this revision, the stack rebased on main c6fa613: pre-commit over the whole PR against main, at every PR of the stack; and at the stack's tip the full tests/unit on transformers 5.18 and 5.5 and on torch 2.8, and the hf_ptq example tests, each with the same failures as main in that environment. CI runs the full unit suite.

  • The GPU results here were measured on earlier revisions of this branch; this revision's GPU tests run in CI only.

  • On GPUs: the Kimi-K3 run ([OMNIML-6079] Add the Kimi-K3 expert-parallel adapter #2749) loads its MXFP4 experts through this path; Kimi-K3's prepare drops quantization_config at both levels itself, so no compressed-tensors quantizer took its experts over.

  • Re-cut on 2026-10-27 (see the note at the top): pre-commit, mypy included, passes at this head over the whole change against main, and the series' PRs merged together give a tree byte-identical to the former stack's tip. This PR's own unit tests pass at this head on CPU (torch 2.14, transformers 5.18); CI runs the full unit suite.

  • Review fixes, 2026-10-27: an unmarked config with nothing to decompress is refused once the hooks are in; only converters under .experts. are rewritten, not a shared expert's. Each behaviour fix has a test that fails without it; pre-commit, mypy included, and this PR's own unit tests pass at this head.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ Additive; installed only by a model's prepare hook.
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A. compressed_tensors is imported only for compressed checkpoints, which already need it; only CI's unit sessions install it, for the tests.
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: N/A. The feature's entry lands with [OMNIML-6085] Run expert-parallel PTQ from hf_ptq.py #2754.
  • Did you get Claude approval on this PR?: ❌ Not yet.

Additional Information

Part 18 of 21 of expert-parallel PTQ for Hugging Face MoE models; merges after #2753.

Summary by CodeRabbit

  • New Features
    • Compressed Mixture-of-Experts weights can be decompressed during model loading, while preserving expert sharding, ordering, and compatible weight data types.
    • Loading local checkpoints can stage missing Python files required by remote model code.
    • Supported vision-language model configurations can be converted into native backbone configurations.
  • Compatibility
    • On-the-fly decompression requires Transformers 5.8 or newer.

@copy-pr-bot

copy-pr-bot Bot commented Oct 12, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review.

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: NVIDIA/Model-Optimizer/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: d5c786aa-aeec-4bdf-a402-a56454302b13
📥 Commits

Reviewing files that changed from the base of the PR and between b769406 and ff2c0bf.

📒 Files selected for processing: 1
  • noxfile.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 7 remain after this review.

Walkthrough

The change adds on-load decompression for compressed-tensors MoE expert weights, hooks for expert ordering and dtype preservation, and a Transformers loader wrapper that stages missing sibling Python files for local checkpoints.

Changes: MoE Weight Decompression

Layer / File(s) Summary
Compression and backbone configuration
modelopt/torch/models/_onfly_decompress.py, tests/unit/torch/models/test_onfly_decompress.py
Adds native backbone config conversion and quantization-config lookup. The lookup selects a registered compressor and its stored parameter names. Tests cover config conversion and compression-group selection.
Expert decompression and loading
modelopt/torch/models/_onfly_decompress.py, tests/unit/torch/models/test_onfly_decompress.py
Rewrites per-expert weight converters to provide stored tensors for decompression. Expert tensors are ordered numerically, and integer source tensors are not cast to floating dtypes. Tests check decompression, fusion, ordering, and dtype behavior.
Hook installation and validation
modelopt/torch/models/_onfly_decompress.py, tests/unit/torch/models/test_onfly_decompress.py, noxfile.py
Adds config marking and process-wide hook installation. Tests cover converter selection and hook lifecycle. The unit Nox session installs compressed-tensors except for the torch_28 and torch_29 matrix entries.

Local Remote-Code Staging

Layer / File(s) Summary
Stage sibling Python files
modelopt/torch/models/_remote_code.py
Wraps Transformers’ cached-module loader to copy missing sibling Python files for local checkpoints. Copy failures produce a warning, and the wrapper returns the original resolved path.

Priority: Low

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Suggested reviewers: kevalmorabia97

Sequence Diagram(s)

sequenceDiagram
  participant TransformersWeightLoader
  participant WeightConverter
  participant MergeModulelist
  participant RegisteredCompressor
  TransformersWeightLoader->>WeightConverter: Provide stored expert tensors
  WeightConverter->>MergeModulelist: Pass anchor and sibling tensors
  MergeModulelist->>RegisteredCompressor: Decompress expert parameters
  RegisteredCompressor-->>MergeModulelist: Return bf16 expert weights
  MergeModulelist-->>WeightConverter: Emit weights under anchor keys
Loading
Merge Risk: Low · up to ff2c0

Multi-rank loads of local checkpoints may fail if a rank imports a sibling Python module while another rank is copying it. This is a bounded startup risk, but it remains open for this merge.

Pre-merge checks: Passed 5
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns Passed PASS. The reviewed changes add no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), hardcoded trust_remote_code=True, external-input eval()/exec(), or # nosec commen…
Title check Passed The title clearly and concisely describes the primary change: on-the-fly decompression of compressed-tensors MoE experts during loading.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Security Anti-Patterns

Explanation

PASS. The reviewed changes add no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), hardcoded trust_remote_code=True, external-input eval()/exec(), or # nosec comments. The PR does not modify pyproject.toml or requirements.txt; its compressed-tensors installation is limited to noxfile.py.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@shengliangxu
shengliangxu added this pull request to stack #2755 October 12, 2026 00:57
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch from 2142693 to 2e134ec Compare October 12, 2026 02:35
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11a-onfly-decompress branch from 95832dd to e243f04 Compare October 12, 2026 02:35
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-2747/

Built to branch gh-pages at 2026-10-27 00:05 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.65471% with 3 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (shengliangx/hfep-13-ep-export@2ec61a3). Learn more about missing BASE report.

Files with missing lines Patch % Lines
modelopt/torch/models/_onfly_decompress.py 98.88% 2 Missing ⚠️
modelopt/torch/models/_remote_code.py 96.87% 1 Missing ⚠️
Additional details and impacted files
@@                       Coverage Diff                        @@
##             shengliangx/hfep-13-ep-export    #2747   +/-   ##
================================================================
  Coverage                                 ?   62.46%           
================================================================
  Files                                    ?      658           
  Lines                                    ?    74106           
  Branches                                 ?        0           
================================================================
  Hits                                     ?    46294           
  Misses                                   ?    27812           
  Partials                                 ?        0           
Flag Coverage Δ
unit 62.46% <98.65%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test e243f04

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch from 2e134ec to fdd217a Compare October 12, 2026 08:39
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11a-onfly-decompress branch from e243f04 to d6afd32 Compare October 12, 2026 08:39
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test d6afd32

@shengliangxu shengliangxu changed the title [12/19] Decompress compressed-tensors MoE experts on the fly as they load [OMNIML-6063] [12/19] Decompress compressed-tensors MoE experts on the fly as they load Oct 12, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11a-onfly-decompress branch 2 times, most recently from 67355f4 to b162532 Compare October 13, 2026 01:32
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch from 0a3e056 to 67f1c8f Compare October 13, 2026 01:32
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test b162532

@shengliangxu
shengliangxu removed this pull request from stack #2755 October 13, 2026 17:42
@shengliangxu
shengliangxu added this pull request to stack #2775 October 13, 2026 17:43
@shengliangxu shengliangxu changed the title [OMNIML-6063] [12/19] Decompress compressed-tensors MoE experts on the fly as they load [OMNIML-6063] [12/20] Decompress compressed-tensors MoE experts on the fly as they load Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9-ep-model-specs branch from 67f1c8f to 2f18815 Compare October 13, 2026 19:38
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11a-onfly-decompress branch from b162532 to 269d3d2 Compare October 13, 2026 19:38
@shengliangxu
shengliangxu removed this pull request from stack #2775 October 13, 2026 19:38
@shengliangxu
shengliangxu changed the base branch from shengliangx/hfep-9-ep-model-specs to shengliangx/hfep-9b-ep-load-routing October 13, 2026 19:39
@shengliangxu
shengliangxu added this pull request to stack #2781 October 13, 2026 19:40
@shengliangxu shengliangxu changed the title [OMNIML-6063] [12/20] Decompress compressed-tensors MoE experts on the fly as they load [OMNIML-6063] [14/21] Decompress compressed-tensors MoE experts on the fly as they load Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11a-onfly-decompress branch from 269d3d2 to dddc2ee Compare October 13, 2026 21:12
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test dddc2ee

@shengliangxu
shengliangxu removed this pull request from stack #2781 October 14, 2026 00:13
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11a-onfly-decompress branch from dddc2ee to c668ec2 Compare October 14, 2026 00:14
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-9b-ep-load-routing branch from 68f620d to 5c5cde0 Compare October 14, 2026 00:14
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 8e71660

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment on lines +338 to +342
if installed is not None:
if compression is None:
# A config its prepare dropped quantization_config from after installing (Kimi-K3's),
# already marked, may be prepared again: nothing new to decompress.
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] The already-installed + compression is None branch returns success without marking hf_config, which is the one outcome the rest of this module refuses everywhere else.

The comment explains the intended case — Kimi-K3's prepare dropping quantization_config after installing, so the config is "already marked". That holds for the two designed flows (install-then-drop on the same object, and to_native_backbone_config carrying the mark). But the branch never checks it, so the same call on a different unmarked config without a quantization_config returns quietly, and the caller believes decompression is active for it. The consequence is the silent one this module is built to avoid: patched finds that config unmarked → returns the stock .weight converters → the compressed experts match no stored key and load newly initialized; and in load_and_prepare_ep, _decompressed_as_loaded is False while _ckpt_quant_method is None (the config has no quantization_config left), so _EP_SOURCE_FORMATS admits it and check_compressed_keys never runs either. Note the asymmetry too: the identical config raises ValueError ("config has no compressed-tensors quantization_config") when nothing is installed yet, so whether a caller mistake is caught depends only on install order.

test_a_checkpoint_compressed_differently_needs_a_process_of_its_own exercises exactly this path (SimpleNamespace(checkpoint=None)) and asserts only that the hooks are untouched — it does not assert the config came out marked, so the hole is currently pinned as accepted.

One line makes the branch assert what its comment already claims:

        if compression is None:
            # A config its prepare dropped quantization_config from after installing (Kimi-K3's),
            # already marked, may be prepared again: nothing new to decompress.
            if not is_marked(hf_config):
                raise ValueError(
                    "install_onfly_decompress: config has no compressed-tensors "
                    "quantization_config and is not marked: nothing would decompress"
                )
            return

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: with the hooks installed, a config without a compressed-tensors quantization_config now returns only if it carries the mark, i.e. it is the config the installing prepare stripped; an unmarked one raises, as it does before the hooks go in. test_a_checkpoint_compressed_differently_needs_a_process_of_its_own now prepares the stripped config again and asserts an unmarked one is refused.

Comment on lines +370 to +373
# only the per-expert MoE converters (have `.weight` sources under `experts`)
if isinstance(c, WeightConverter) and any(
"experts" in p and p.endswith((".weight", ".weight$")) for p in c.source_patterns
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] The converter filter matches the bare substring "experts", which also matches shared_experts, and the rewritten guard below cannot catch the over-match.

check_compressed_keys is careful to use ".experts." precisely so Kimi's mlp.shared_experts.* (no dot before experts) stays out of the routed-expert set — this filter is the one place that distinction is dropped. If a release's conversion mapping ever carries a WeightConverter with .weight sources under shared_experts (a fused gate/up for the shared MLP is the obvious candidate), it gets rewritten to *.weight_packed$ etc., those keys do not exist in a checkpoint that compresses only the routed experts, and the shared MLP loads newly initialized behind a missing-keys warning. rewritten is already ≥ 1 from the real expert converters, so the RuntimeError below never fires, and check_compressed_keys inspects the checkpoint rather than the converters, so nothing else notices.

Two options, either cheap:

  • Tighten the string to match what check_compressed_keys means: ".experts." in p and p.endswith((".weight", ".weight$")).
  • Better, match structurally on the property _make_op_class actually depends on — isinstance(c.operations[0], MergeModulelist), the same operations[0] test the loader uses to assign tensor_idx. That is also the convention the neighbouring block-FP8 fix in model_load_utils.py:1987-1993 already uses (isinstance(ops[0], Fp8Dequantize) and isinstance(ops[1], MergeModulelist)) rather than pattern strings, so it keeps the two mechanisms reading the mapping the same way.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with the first option: the filter now matches .experts., as check_compressed_keys does, so a converter fusing a shared expert's .weight tensors is left alone. I kept a source-pattern test rather than operations[0], since _compressedify depends on the .weight sources. test_install_rewrites_only_the_per_expert_weight_converters now includes a shared_experts gate/up converter and fails without the fix.

@claude

claude Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Claude review (round 7) — [18/21] on-the-fly compressed-tensors MoE decompression

Scope: full review (the trigger comment carried no scoping instructions). 7 changed files, all reviewed: modelopt/torch/models/_onfly_decompress.py (+459), _remote_code.py (+67), modelopt/torch/utils/plugins/model_load_utils.py (+28/-1), noxfile.py (+6/-1), and the three test files (test_onfly_decompress.py +729, test_remote_code.py +86, test_ep_loader.py +58).

Findings: CRITICAL: 0 · IMPORTANT: 0 · SUGGESTION: 2

Since round 6

The diff is byte-identical to the one round 6 reviewed — the four commits were re-cut on 2026-10-27 when the series became a tree, and git diff origin/shengliangx/hfep-9-ep-model-specs HEAD reproduces the same 1433 insertions. I re-derived the review rather than carrying the verdict forward; both round-5 fixes still read correctly in the current source:

  • The rewritten counter plus the post-loop delattr over _config_levels keeps a marked model whose mapping has nothing to rewrite loud (RuntimeError) and still marked, instead of silently falling back to the stock .weight converters.
  • check_compressed_keys's dtype pass closes the wide-float hole by refusal: F16/F32/F64 are rejected, and the accepted set (BF16, the fp8 variants, F8_E8M0, the integer dtypes) is exactly what bf16 holds exactly, so an nvfp4 fp32 weight_global_scale or an fp16 scale cannot reach decompress already rounded.

I also re-checked the round-4 ignore finding against the current code and consider it closed, by the checkpoint-side check rather than the config-side one: targets:["Linear"], ignore:["lm_head"] now trips outside (the attention weight_packed stems carry no .experts.), and targets:["Linear"], ignore:["re:.*experts.*"] trips it too, since the experts end up in plain/not compressed while everything else is compressed. Both shapes are refused before from_pretrained.

This round (both non-blocking)

  1. The already-installed + nothing-to-decompress branch returns success without marking the config (inline). The comment asserts the config is "already marked", which holds for the two designed flows but is never checked — so an unmarked config without a quantization_config passes install quietly, then gets stock converters, and load_and_prepare_ep admits it as unquantized (_decompressed_as_loaded false, _ckpt_quant_method None) so check_compressed_keys never runs either. The same config raises ValueError when nothing is installed yet, so whether the mistake is caught depends on install order. test_a_checkpoint_compressed_differently_needs_a_process_of_its_own walks this path and asserts only that the hooks are untouched.
  2. The expert-converter filter matches the bare substring "experts", so shared_experts matches too (inline). check_compressed_keys deliberately uses ".experts." to keep Kimi's shared MLP out of the routed set; this is the one place that distinction is dropped, and the rewritten guard cannot catch an over-match. Matching structurally on isinstance(c.operations[0], MergeModulelist) — the property _make_op_class actually depends on, and the convention the neighbouring block-FP8 fix at model_load_utils.py:1987 already uses — would be both tighter and more consistent.

What checked out this round

  • Decompression mechanism. new_sources is grouped anchor-param-first, so gate precedes up within the weight_packed block and convert's list(input_dict.keys()) walk emits the anchor keys in that order for the downstream Concatenate. The endswith(anchor) test stays unambiguous in the format: float-quantized case where the anchor is .weight itself, since .weight_scale does not end with .weight. Siblings are drawn from param_names[1:] only, and non-anchor patterns continue before any pop, so no key is popped twice; the optional-sibling pop(..., None) is what lets Kimi-K3 store a subset of compression_param_names. Unconsumed sources raise rather than drift into the merge.
  • Expert pairing and EP. The per-param length check against anchor_list is the guard that keeps positional weight_packed ↔ weight_scale pairing honest, and the anchor_list[i] is not None filter bounds peak memory at the per-rank shard while keeping index alignment (a sibling left unsharded still indexes correctly, since None entries preserve list length). MergeModulelist subclassing is the right lever for tensor_idx, for the same reason _DequantThenMerge exists in model_load_utils.py.
  • Mark lifecycle. install_onfly_decompress marks, to_native_backbone_config propagates from either level, is_marked and patched read and clear all of _config_levels (("text_config", "llm_config")), and the caller's config keeps its mark while model.config — the copy a later save_pretrained writes — comes out cleared. copy.deepcopy carries the tuple-valued mark into load_and_prepare_ep's private config as needed.
  • load_and_prepare_ep. The format waiver and the quantization_config deletion sit on exactly the marked path; the deletion covers all three levels _ckpt_quant_method reads, uses del rather than = None for pre-5.11 behaviour, and runs on the deepcopy — test_a_compressed_checkpoint_a_spec_decompresses_as_it_loads_passes pins all of that plus the caller's config surviving untouched. check_compressed_keys runs after _resolve_checkpoint_dir and before from_pretrained, so a refusal leaves no partial load, and it issues no collectives and reads the same headers on every rank, so the refusal is rank-uniform.
  • _remote_code.py. Post-orig copy into the already-resolved stage dir, HF_MODULES_CACHE join for a relative resolve, shutil.copy onto the temp file so the mode comes from the source rather than mkstemp's 0600, atomic os.replace, idempotent, top-level *.py only as documented. The new test_a_module_is_staged_whole_or_not_at_all pins the half-written-module case that the multi-rank race depends on. (A cut-short copy still leaves the mkstemp file behind, but with no .py suffix it is inert on sys.path — noted in an earlier round, not worth a new thread.)
  • Patch target. transformers.modeling_utils.get_model_conversion_mapping is the binding from_pretrained resolves, while modelopt's export-side callers import from transformers.conversion_mapping and keep the stock mapping. _install_keep_int_native delegates to the original with keyword device/dtype, so it does not restate upstream's body.
  • noxfile. The ("torch_28", "torch_29") ids match real TORCH_VERSIONS keys and the tf_pins install follows, so each leg keeps its transformers pin; keeping compressed-tensors out of dev-test is what protects the transformers-free partial_unit lane. (Denylist rather than allowlist, noted previously.)
  • Tests. CPU-only, tiny safetensors fixtures, real-library cases gated on compressed_tensors plus transformers 5.8 — appropriate for tests/unit under CLAUDE.md's budget.

Assessment

Risk: low. The mechanism, its admission criteria and its mark lifecycle now agree with each other end to end, and the two remaining items are both "a caller mistake outside the designed flows would be accepted quietly" rather than anything a passing Kimi run would hit. Neither blocks merge.

Verification note: neither transformers nor compressed_tensors is installed in this sandbox. The WeightConverter field set, _materialize_copy's exact signature, the classmethod shape of compression_param_names / decompress, and whether the loader really hands None over for non-local experts are read from the diff, its comments and the tests — CI's tf_latest × torch>=2.10 legs and the GPU Kimi-K3 run remain the real check on those. Both findings above are verified against the checked-in source alone.

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude review passed — no blocking issues found. LGTM

Expert-parallel PTQ loads a checkpoint with from_pretrained over the
expert-parallel mesh, so each rank builds only its own experts. A
checkpoint whose experts are compressed-tensors quantized -- Kimi-K2.5's
INT4, Kimi-K3's MXFP4 -- cannot take that load as it is: transformers'
fused-experts conversion expects each expert's plain .weight.

_onfly_decompress rewrites those per-expert converters to read the
checkpoint's stored keys (weight_packed, weight_scale, ...) and prepends a
step that dequantizes each owned expert to bf16 before the merge, so PTQ
runs on the original compressed checkpoint with no decompressed copy on
disk. It is format-agnostic: the compressed_tensors library does the math,
driven by the checkpoint's own quantization_config. It needs transformers
5.8. It also holds the helper that rebuilds a VLM's bundled text config as
a native transformers backbone config.

_remote_code.stage_full_remote_closure makes transformers stage every
module of a local checkpoint's remote code, not only those one import
deep, which loading the Kimi checkpoints' own modeling needs.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The module skipped below 5.6, but the code under test requires 5.8 and
raises below it, so a 5.6 or 5.7 run failed instead of skipping.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The decompress hooks are process-wide and keep the first checkpoint's
compressor, scheme and stored params, so a second checkpoint compressed
differently was decompressed with the first one's settings without a word.
Installing again now compares the compression: the same one is a no-op, as
is a config its installing prepare already stripped of quantization_config,
and a different one raises.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The installed decompressor rewrote the per-expert converters of every model
from_pretrained loaded afterwards. An uncompressed MoE in the same process had
its .weight sources renamed to the first checkpoint's packed names, matched no
stored key, and loaded its experts newly initialized without an error.
Installing now marks the config it is given, a repeat install for the same
checkpoint marks its config too, and only a model built from a marked config
gets the rewritten converters. from_pretrained loads from a deep copy of the
config, which keeps the mark; the rewrite removes it from that copy so a later
save does not write it into config.json.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
They drive the real compressed-tensors registry and compressor, so they
skip where the library is missing, as it is in a plain dev-test install.
CI's unit session installs it; the skip reason now says so instead of
leaving only an import error. With compressed-tensors 0.19.0 all four
cases pass.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The real-library tests of on-the-fly expert decompression skip without
compressed-tensors, and no CI environment installs it, so CI never ran
them. The unit sessions now install it with the leg's torchvision pin,
so the resolver keeps the leg's torch. compressed-tensors 0.19 needs
torch>=2.10, so the torch 2.8 and 2.9 legs leave it out: there it would
upgrade torch or, held back by the pin, fall back to an old 0.15 release.

It stays out of the dev-test extra: the transformers-free
partial_unit(torch) lane installs that extra, and compressed-tensors
pulls in transformers.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The two tests that restated _compressedify's source patterns now ride
the packed-load test, parametrized over the stored-param sets
(pack-quantized; weight and weight_scale): a swapped gate/up, an
unanchored pattern and a decompress op that is not a MergeModulelist
each fail it. The other-model test and the copy-from_pretrained test
become one test over the three loads. The helper-level ValueError test
goes: the install-level test asserts the same error and that nothing was
patched. The non-weight-sources and native-backbone guard tests stay,
since nothing else catches a regression in their branches.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… check

A model spec that installs the on-the-fly decompressor marks its
config. Kimi-K2.5's keeps its compressed-tensors quantization_config at
the top level, where the decompressor reads it, so the expert-parallel
load's format check would refuse a checkpoint it does decompress. A
marked config now passes; an unmarked compressed-tensors checkpoint is
still refused.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The decompressor read "no quantization_config" off a ValueError, but compressed_tensors raises
pydantic's ValidationError, also a ValueError, for a quantization_config it cannot parse (say, one
written by a newer compressed-tensors). Taken for none, the second install of a process returned
without marking the config, and the checkpoint's experts would load newly initialized. Reading a
config with none now returns None, so a parse error goes through.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…ring patch transformers made moot

The rewritten get_model_conversion_mapping forwards its arguments as it gets them: pinning two of
them as keywords made a fourth positional collide with them. The rewritten expert converter keeps
the scope transformers sets on a converter after building it (scope_prefix, base_model_prefix,
force_cpu, whichever the release has), and the integer-keeping copy hook calls transformers' own
copy instead of standing in for it.

The patch that re-sorted experts numerically is gone: every transformers from 5.5 sorts the state
dict by dot_natural_key before collecting, so experts already arrive in numeric order, and the
patch only kept a process-wide id() map. stage_full_remote_closure gets a unit test, the install
message prints on rank 0 only, and the module docstrings keep the contract, not the design notes.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
_read_compression took the config group whose targets mention "expert", else the first, as the
experts' scheme. Only the expert converters are rewritten, so another group's weights loaded newly
initialized, and a guessed group decompressed the experts with the wrong bits, group size or zero
point. A checkpoint with more than one group is now refused before anything is patched. The Kimi
checkpoints this path serves carry one group each (Kimi-K2.5, K2.6 and K2.7-Code INT4
pack-quantized, Kimi-K3 MXFP4), whose ignore list leaves everything but the routed experts in bf16.

DecompressExperts handed a checkpoint key it did not consume on to the merge, which would fuse it
into the wrong slot. It now raises, which transformers reports as a conversion error that fails the
load.

install_onfly_decompress checks the transformers 5.8 floor before it imports core_model_loading, so
an older release gets that message rather than an ImportError, and _mark no longer writes a dict key
no reader looks at.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Kimi-K2.5's config class copies its text config's quantization_config to the top level, where the
text_config swap leaves it, so the expert-parallel load handed from_pretrained a marked config that
still carried it. That builds transformers' compressed-tensors quantizer, whose own expert
decompression (from 5.13) claims the per-expert converters before the decompressor's hook sees them.
On the marked path the load now deletes quantization_config from its copy of the config, at the top
level and on text_config; deleted rather than set to None, which transformers before 5.11 takes for
a quantized checkpoint.

The decompressor's mark has one scope now: a config counts as marked when it or its text_config
carries the mark, the hook reads it that way and clears it at both levels, and
to_native_backbone_config carries it to the backbone config it derives. The hook read the top level
only, so a VLM whose text_config was marked loaded stock converters while the format check passed
it, and a backbone loaded on its own from the derived config was never rewritten.

The checkpoint's one config group must target the experts (a class target or a regex naming them,
as transformers' own compressed-tensors loader finds the experts' group), and a quantization_config
with no groups is refused as nothing to decompress rather than with the several-groups message.

stage_full_remote_closure copies each module to a temp file in the stage dir and renames it into
place, so a rank never imports a module another rank is still writing.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The on-the-fly decompressor rewrites only the experts' converters, and the expert-parallel load
drops the quantization_config that would load any other compressed weight. The checkpoint's one
config group can target a class such as Linear, which does not say which modules it compressed:
the common compressed-tensors layout compresses every Linear but lm_head, and an ignore list can
leave the experts plain instead. Either way part of the model loaded newly initialized behind a
missing-keys warning. The load now checks the names the checkpoint stores before anything loads:
every weight stored compressed must be an expert's, and every expert's weight compressed. The mark
now holds the names a compressed weight is stored under, which the check reads off the config.
The Kimi-K2.5, K2.6, K2.7-Code and K3 checkpoints pass it.

install_onfly_decompress refuses a raw dict before it patches anything, since the load finds its
hooks by a mark a dict cannot carry, and the load deletes the quantization_config of llm_config too,
every level _ckpt_quant_method reads.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Three ways the on-the-fly decompression could still load a compressed checkpoint wrong without a
word are now refused:

- The conversion-mapping hook rewrote whatever per-expert converters it found, possibly none, and
  cleared the config's mark before rewriting. A marked model whose mapping has no per-expert
  converter to rewrite, whose experts would load newly initialized, is now refused, and the mark is
  cleared only once the converters are rewritten.
- The load may cast a source tensor to the model's dtype before the decompression reads it; only
  integers are kept as stored. The expert-parallel load's check of what the checkpoint stores now
  also reads the dtypes of the compressed experts' tensors and refuses one bf16 cannot hold exactly
  (fp16, fp32, fp64), such as NVFP4's fp32 global scale. The Kimi-K2.5, K2.6, K2.7-Code and K3
  checkpoints store int32, uint8 and bf16 there.
- DecompressExperts pairs each expert's stored tensors by position, and now refuses stored params
  that disagree on how many experts they hold.

The decompressor also finds its compressed-tensors config and mark on llm_config, as the rest of
the expert-parallel load does, and the integer-keeping loader hook says why it stays process-wide.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…re in

With the hooks already installed, a config without a compressed-tensors quantization_config returned
as if its load would decompress. Only a config the installing prepare stripped, which carries the
mark, is the same checkpoint again; an unmarked one would load its experts newly initialized, and
is now refused as it is before the hooks go in.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The filter matched "experts" anywhere in a source, so a converter fusing a shared expert's .weight
tensors (mlp.shared_experts.*) would be rewritten to read compressed keys the checkpoint does not
store, and the shared MLP would load newly initialized. It now matches .experts., as
check_compressed_keys does.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 00e5afe

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment on lines +378 to +380
if isinstance(c, WeightConverter) and any(
".experts." in p and p.endswith((".weight", ".weight$")) for p in c.source_patterns
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] The ".experts." in p test requires a dot before experts, so a conversion mapping whose per-expert source pattern starts at the expert list — "experts.*.gate_proj.weight" rather than "mlp.experts.*.gate_proj.weight" — matches nothing. rewritten then stays 0 and the load hard-fails with "no per-expert converter to rewrite", for a checkpoint this path should handle. (It fails loudly rather than silently, so this is low severity; every mapping in transformers today carries the mlp. prefix.)

A boundary-aware test keeps the shared_experts. exclusion that motivated the current form while also accepting an unprefixed pattern:

import re

_EXPERTS_SEGMENT = re.compile(r"(?:^|\.)experts\.")
...
            if isinstance(c, WeightConverter) and any(
                _EXPERTS_SEGMENT.search(p) and p.endswith((".weight", ".weight$"))
                for p in c.source_patterns
            ):

check_compressed_keys can keep the plain ".experts." in name test — checkpoint tensor names are always fully qualified, so the leading dot is always there.

Comment on lines +331 to +332
compression = _read_compression(hf_config)
require_transformers("5.8", "on-the-fly compressed-tensors decompression")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] _read_compression imports compressed_tensors and parses the config before the transformers floor is enforced, so on transformers 5.5 (which CI exercises) a user with a compressed checkpoint but no compressed_tensors installed sees ModuleNotFoundError: compressed_tensors instead of the actionable "requires transformers >= 5.8". Swapping the two lines keeps the "no quantization_config → ValueError without needing compressed_tensors" property intact, since require_transformers imports nothing but transformers:

Suggested change
compression = _read_compression(hf_config)
require_transformers("5.8", "on-the-fly compressed-tensors decompression")
require_transformers("5.8", "on-the-fly compressed-tensors decompression")
compression = _read_compression(hf_config)

(The one behavioural difference is that a config with nothing to decompress now also needs transformers 5.8 to reach its ValueError — which is already a hard requirement of every path that reaches this function.)

Comment on lines +52 to +63
try:
for py in glob.glob(os.path.join(src, "*.py")):
dst = os.path.join(stage_dir, os.path.basename(py))
if not os.path.exists(dst):
# Every rank loading the checkpoint stages into this dir and imports from it:
# renamed into place (atomic), a file appears there whole, never half written.
fd, tmp = tempfile.mkstemp(dir=stage_dir)
os.close(fd)
shutil.copy(py, tmp)
os.replace(tmp, dst)
except OSError as e: # a failed copy only matters if a deeper import needs the file
warnings.warn(f"Could not stage the remote code of {src}: {e}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] Two small things here:

  1. The docstring promises "every module of a local checkpoint's remote code", but glob("*.py") only stages the flat siblings. Remote code that imports from a subdirectory (from .kernels.foo import bar) is still staged incomplete and still fails in get_relative_import_files — the exact symptom this function exists to fix. Either recurse while preserving the relative path, or narrow the docstring to say flat siblings only (which is what the Kimi checkpoints need).

  2. On an OSError from shutil.copy the mkstemp file is never removed, so each failed attempt leaks a tmpXXXXXX file into the shared stage dir. Wrapping the copy so the temp file is cleaned up on failure keeps the dir clean across retries/ranks:

            for py in glob.glob(os.path.join(src, "*.py")):
                dst = os.path.join(stage_dir, os.path.basename(py))
                if os.path.exists(dst):
                    continue
                # Every rank loading the checkpoint stages into this dir and imports from it:
                # renamed into place (atomic), a file appears there whole, never half written.
                fd, tmp = tempfile.mkstemp(dir=stage_dir)
                os.close(fd)
                try:
                    shutil.copy(py, tmp)
                    os.replace(tmp, dst)
                except OSError as e:  # a failed copy only matters if a deeper import needs the file
                    os.unlink(tmp)
                    warnings.warn(f"Could not stage the remote code of {src}: {e}")

This also narrows the try to the operation that can fail, instead of swallowing an OSError from glob/os.path.exists too.

Comment on lines +2431 to +2437
for cfg in (
hf_config,
getattr(hf_config, "text_config", None),
getattr(hf_config, "llm_config", None),
):
if cfg is not None and hasattr(cfg, "quantization_config"):
del cfg.quantization_config

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] The literal (hf_config, text_config, llm_config) triple is now spelled out in a fourth place: moe._SUB_CONFIG_ATTRS, _onfly_decompress._config_levels, _ckpt_quant_method (line 2155) and here. The comment correctly says "every level _ckpt_quant_method reads is cleared", but that invariant is only maintained by hand — a model that attaches its LM config under a third name would add it to _SUB_CONFIG_ATTRS (the one place moe.py validates against) and silently leave a quantization_config here for transformers' quantizer to pick up.

Driving it off the shared constant removes the drift:

        from modelopt.torch.utils.moe import _SUB_CONFIG_ATTRS

        for cfg in (hf_config, *(getattr(hf_config, a, None) for a in _SUB_CONFIG_ATTRS)):
            if cfg is not None and hasattr(cfg, "quantization_config"):
                del cfg.quantization_config

_ckpt_quant_method above could take the same treatment in a follow-up.

@claude

claude Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Claude review (round 8) — [18/21] on-the-fly compressed-tensors MoE decompression

Scope: full review (the trigger comment carried no scoping instructions). All 7 changed files opened: modelopt/torch/models/_onfly_decompress.py (+466), _remote_code.py (+67), modelopt/torch/utils/plugins/model_load_utils.py (+28/-1), noxfile.py (+6/-1), and the three test files. Diff taken two-dot against origin/shengliangx/hfep-9-ep-model-specs.

Findings — CRITICAL: 0 · IMPORTANT: 0 · SUGGESTION: 4

# File Finding
1 _onfly_decompress.py:378 ".experts." in p needs a leading dot, so an unprefixed experts.*.gate_proj.weight source pattern would fail the load with "no per-expert converter to rewrite"
2 _onfly_decompress.py:331 _read_compression runs before require_transformers("5.8"), so a too-old transformers surfaces a compressed_tensors ImportError instead of the version floor
3 _remote_code.py:52 glob("*.py") stages flat siblings only, not "every module" as the docstring says; a failed shutil.copy leaks its mkstemp file
4 model_load_utils.py:2431 the (top, text_config, llm_config) level list is now spelled out in a fourth place instead of riding _SUB_CONFIG_ATTRS

All four are non-blocking.

What I traced and found sound

  • Silent-wrongness paths. Every way an expert could end up newly initialized is closed by a loud failure: rewritten == 0 raises; a weight compressed outside the experts or an expert stored plain is refused by check_compressed_keys before anything loads; an orphan sibling (a weight_scale whose weight_packed is absent) trips the per-expert count check; and an unconsumed source key raises in DecompressExperts.convert rather than reaching the merge. With the quantization_config dropped at every level, the dense weights are guaranteed plain by that same check, so nothing else needs the quantizer.
  • Shared experts. .shared_experts. is excluded identically by the converter filter and by check_compressed_keys (the leading-dot test rules out shared_experts), so a checkpoint that also compresses a shared expert is refused as outside rather than half-rewritten. The two sites agree, which is what the last commit was fixing.
  • Positional pairing. Anchor and sibling lists are both produced by the same tensor_idx sharding, so the anchor_list[i] is not None filter and lists[pn][i] stay aligned; the length check catches the one layout that would otherwise pair expert n's weight with expert n+1's scale.
  • gate/up ordering. new_sources groups by param_names with gate-before-up preserved inside each group, and out is built in input_dict order, so the downstream Concatenate fuses in the same order it would for an uncompressed checkpoint.
  • dtype exactness. _install_keep_int_native only suppresses float-to-int casts (so packed weights, shapes, zero-points and g_idx survive), and the F16/F32/F64 refusal in check_compressed_keys closes the complementary hole where a float the load would round to bf16 scales every weight. The process-wide scope of that patch is documented, and its precondition (integer source for a float param, no quantizer) is specific to this path.
  • Distributed safety. check_compressed_keys is a rank-local, deterministic header read with no collectives, run after _resolve_checkpoint_dir, so all ranks accept or refuse together. Peak memory stays at one expert at a time per rank, and only locally-owned experts reach the compressor.
  • Mark lifecycle. install_onfly_decompress marks the caller's config, from_pretrained loads from a deep copy, and patched clears the mark at every level only after the rewrite succeeds, so a saved config carries no mark and load_and_prepare_ep's two _decompressed_as_loaded checks both fall before from_pretrained and still see it.
  • Backward compatibility. Purely additive: two new private modules, one private helper, and one and not _decompressed_as_loaded(...) clause that can only widen what the EP load accepts, and only for a config a model spec explicitly marked. No modelopt_state, mode registration or public API touched; all transformers and compressed_tensors imports are function-local.
  • noxfile. compressed-tensors installed alongside the torchvision pin keeps the leg's torch, and keeping it out of dev-test is right: it pulls in transformers, which the partial_unit(torch) lane must not get.
  • Tests. The restore_loader_hooks fixture undoes both process-wide hooks via monkeypatch, so the installs do not leak into later tests in the same pytest process, which is the main hazard with a patch of this shape. The new test_ep_loader case asserts the stronger property (the caller's config keeps its own quantization_config while from_pretrained sees none at any level, llm_config included) rather than just the happy path.

No prompt-injection-shaped content in the diff.

Risk: low. The feature is reachable only from a model spec's prepare hook, every unsupported layout is refused before weights are touched, and the refusals are backed by mutation-checked tests. The residual risk is concentrated in the two process-wide transformers patches, both documented and both reverted in tests.

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude review passed — no blocking issues found. LGTM

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant